local rtk = require("libluartk")
local bit64 = rtk.bit64
gx = require("libgamex")
B = require("wxExt.ui_builder")
local dbg = require("wxExt.debug")

local font = wx.wxFont(
	10, -- 字体大小
	wx.wxFONTFAMILY_TELETYPE, -- 等宽字体族
	wx.wxFONTSTYLE_NORMAL, -- 正常样式
	wx.wxFONTWEIGHT_NORMAL, -- 正常粗细
	false -- 非斜体
)

--[[
local TMemoryCtrl=B.TText{
	'',
	readonly=true,multiline=true,
	minsize={660,-1},
	updateMemory=function(self,addr,buf)
		if buf then
			self.Value=gx.hexdump(buf, addr)
		else
			self.Value='<void>'
		end
	end,
	doCreate=function(self)
		self:SetFont(font)
	end
}
]]

local function makeGridTitle()
	local cols = {}
	local i
	for i = 1, 16 do
		local title = string.format("%02X", i - 1)
		table.insert(cols, title)
		if i == 8 then
			table.insert(cols, "-")
		end
	end
	table.insert(cols, "Text        ")
	return cols
end

local TMemoryGrid = B.TGrid({
	cols = makeGridTitle(),
	rows = 32,
})

function TMemoryGrid:doCreate()
	self:SetDefaultCellFont(font)
	self:DisableDragColSize()
	self:DisableDragRowSize()
	self:SetRowLabelSize(130)
end

function TMemoryGrid:onCellChanged(evt)
	local row, col = evt:GetRow(), evt:GetCol()
	print(row, col)
	local val = self:GetCellValue(row, col)
	val = tonumber("0x" .. val)
	val = string.char(val)
	if col > 8 then
		col = col - 1
	end
	local addr = self.FAddress + row * 16 + col
	local ok, err = gx:poke(addr, val)
	print(string.format("0x%x", addr), ok, err)
end

function TMemoryGrid:onKeyDown(evt)
	local code = evt:GetKeyCode()
	if code == wx.WXK_PAGEUP then
		rtk.printk(0, "Page up\n")
		self:updateMemory(self.FAddress - 0x100)
	elseif code == wx.WXK_PAGEDOWN then
		rtk.printk(0, "Page down\n")
		self:updateMemory(self.FAddress + 0x100)
	elseif code == wx.WXK_RETURN then
		if self:IsCellEditControlEnabled() then
			self:DisableCellEditControl()
		end
		local row, col = self:GetGridCursorRow(), self:GetGridCursorCol()
		if col < 7 then
			col = col + 1
		elseif col == 7 then
			col = 9
		elseif col < 0x10 then
			col = col + 1
		else
			col = 0
			row = row + 1
		end
		self:SetGridCursor(row, col)
		return
	else
		evt:Skip()
	end
end

function TMemoryGrid:updateMemory(addr)
	local i
	local pbyte = 1
	self.FAddress = bit64.band(addr, bit64.bnot(0xf))
	self.FCellAddress = addr
	rtk.printk(0, "Dump memory at %x\n", self.FAddress)
	local buf = gx:peek(self.FAddress, 512)
	local addr = self.FAddress
	self:Freeze()
	for i = 1, 32 do
		local j
		self:SetRowLabelValue(i - 1, string.format("%016X", addr))
		addr = addr + 0x10
		local ascii_part = ""
		for j = 1, 8 do
			if pbyte > #buf then
				break
			end
			local byte = string.byte(buf, pbyte)
			self:SetCellValue(i - 1, j - 1, string.format("%02X", byte))
			ascii_part = ascii_part .. (byte >= 32 and byte <= 126 and string.char(byte) or ".")
			pbyte = pbyte + 1
		end
		ascii_part = ascii_part .. " "
		for j = 10, 17 do
			if pbyte > #buf then
				break
			end
			local byte = string.byte(buf, pbyte)
			self:SetCellValue(i - 1, j - 1, string.format("%02X", byte))
			ascii_part = ascii_part .. (byte >= 32 and byte <= 126 and string.char(byte) or ".")
			pbyte = pbyte + 1
		end
		self:SetCellValue(i - 1, 17, ascii_part)
	end
	self:AutoSizeColumns(true)
	self:Thaw()
end

function TMemoryGrid:onGridCellLeftClick(evt)
	local row, col = evt:GetRow(), evt:GetCol()
	if col > 8 then
		col = col - 1
	end
	self.FCellAddress = self.FAddress + row * 16 + col
	rtk.printk(0, "row=%d,col=%d,Selected %x\n", row, col, self.FCellAddress)
	evt:Skip()
end

local TMemoryCtrl = B.TPanel({
	dir = "horizontal",
	TMemoryGrid({
		id = "gridMemory",
		expand = true,
		proportion = 1,
	}),
	B.TVBox({
		expand = true,
		B.TLabel({ "Interpreters" }),
		B.TCombo({ id = "cmbInterpreters", expand = true, readonly = true }),
		--			B.TStretcher{expand=true,proportion=1},
		B.TButton({ id = "btnInterprete", title = "Go" }),
	}),
})

function TMemoryCtrl:doInterprete(intp, addr)
	intp = intp or 1
	intp = self.FInterpreters[intp]
	addr = addr or self.FCellAddress
	intp.func(addr, self.F_Top)
end

function TMemoryCtrl:btnInterprete_onClick()
	local sel = self.cmbInterpreters:GetSelection()
	if sel < 0 then
		return
	end
	self:doInterprete(sel + 1, self.gridMemory.FCellAddress)
end

function TMemoryCtrl:doAddInterpreter(intp)
	self.cmbInterpreters:Append(intp.title)
	table.insert(self.FInterpreters, intp)
end

function TMemoryCtrl:go(addr)
	self.gridMemory:updateMemory(addr)
end

function TMemoryCtrl:doCreate()
	self.FInterpreters = {}
end

local Actions = B.TActionList({
	{ id = "IDM_LOAD_SEARCH", title = "Load search\tF2" },
	{ id = "IDM_SAVE_SEARCH", title = "Save search\tF3" },
	{ id = "IDM_FILE_EXIT", title = "Exit" },
	{ id = "IDM_ABOUT", title = "About" },
})

local saved_searches = {}

local TLoadSearchDialogDef = B.TDialog({
	title = "Select search",
	B.TList({ id = "addresses", expand = true, proportion = 1 }),
	B.THBox({
		B.TButton({ wid = wx.wxID_OK, title = "Ok" }),
		B.TButton({ wid = wx.wxID_CANCEL, title = "Cancel" }),
	}),
	minsize = { 300, 500 },
	doCreate = function(self)
		rtk.table_ieach(saved_searches, function(_, s)
			self.addresses:Append(s.name)
		end)
	end,
})

Actions.IDM_LOAD_SEARCH.cmd = function(frame)
	local sel = TLoadSearchDialogDef:create(frame)
	local ret = sel:ShowModal()
	if ret == wx.wxID_CANCEL then
		return
	end
	local idx = sel.addresses.Selection + 1
	frame:updateAddressList(saved_searches[idx].addrs)
end

Actions.IDM_SAVE_SEARCH.cmd = function(frame)
	local name = "search " .. (#saved_searches + 1)
	name = wx.wxGetTextFromUser("请输入字符串：", "输入名字", name)
	if not name then
		return
	end
	local addrs = rtk.table_clone(gx.FAddresses)
	table.insert(saved_searches, { name = name, addrs = addrs })
end

Actions.IDM_FILE_EXIT.cmd = function(frame)
	frame:Close()
end

Actions.IDM_ABOUT.cmd = function(frame)
	local TAboutGamex = B.TDialog({
		title = "About",
		minsize = { 500, 500 },
		B.TWebView({
			id = "html",
			expand = true,
			proportion = 1,
		}),
		B.THBox({
			B.TButton({ wid = wx.wxID_OK, title = "Ok" }),
			B.TButton({ wid = wx.wxID_CANCEL, title = "Cancel" }),
		}),
		html_onWebviewLoaded = function(self, evt)
			if not self.FLoaded then
				self.FLoaded = true
				self.html:SetPage(
					[[
				<html>
				<body>
				跨平台的游戏修改工具
				<a href="https://gitee.com/wangdm001/games">https://gitee.com/wangdm001/games</a>
				</body>
				</html>
				]],
					""
				)
			end
		end,
	})
	local dlg = TAboutGamex:create(frame)
	dlg:ShowModal()
end
--if not Actions.IDM_ABOUT.cmd() then return end

local IDM_MAIN_MENU = B.TMenuBar({
	actions = Actions,
	B.TPopupMenu({
		title = "文件(&F)",
		"IDM_LOAD_SEARCH",
		"IDM_SAVE_SEARCH",
		"IDM_FILE_EXIT",
	}),
	B.TPopupMenu({
		title = "帮助(&H)",
		"IDM_ABOUT",
	}),
})

local TMainDialogDef = B.TFrame({
	sizeable = true,
	minsize = { 848, 600 },
	title = "Game changer",
	menu = IDM_MAIN_MENU,
	B.THBox({
		expand = true,
		B.TLabel({ "Processes", minsize = { 70, -1 } }),
		B.TCombo({ id = "cmbProcesses", readonly = true, proportion = 1 }),
		B.TButton({ title = "刷新(&P)", id = "btnRefreshProcesses" }),
	}),
	B.THBox({
		expand = true,
		B.TLabel({ "String:", minsize = { 70, -1 } }),
		B.TText({ id = "txtString", proportion = 1 }),
		B.TCombo({ id = "cmbType", readonly = true, minsize = { 100, -1 } }),
		B.TButton({ id = "btnSearch", title = "搜索(&S)" }),
		B.TButton({ id = "btnRefine", title = "再次搜索(&F)" }),
		B.TButton({ id = "btnUpdate", title = "更新" }),
	}),
	B.TVBox({
		B.TList({
			id = "lstAddresses",
			expand = true,
			minsize = { -1, 120 },
		}),
		TMemoryCtrl({ id = "memEditor", proportion = 1, expand = true }),
		expand = true,
		proportion = 1,
	}),
	dbg.ID_DEBUG_BAR({ id = "dbar" }),
})

local function parse_any(str)
	local env = {}
	rtk.table_ieach({ 8, 16, 32 }, function(_, w)
		env["i" .. w] = gx["write_int" .. w]
		env["ui" .. w] = gx["write_uint" .. w]
	end)
	env.iconv = iconv
	env.gbk = function(s)
		return rtk.iconv("utf-8", "gb2312", s)
	end
	local code, err
	code, err = load("return " .. str, "", "t", env)
	if not code then
		error("Load error:" .. err)
	end
	str, err = pcall(code)
	if not str then
		error("Exec error:" .. err)
	end
	return err
end

function TMainDialogDef:getSearchString()
	local str = self.txtString.Value
	local tp = self.cmbType.Value
	if tp == "lua" then
		return parse_any(str)
	elseif tp == "string" then
		return str
	elseif tp == "string(gbk)" then
		return rtk.iconv("utf-8", "gb2312", str)
	elseif tp == "hex" then
		return gx.string_from_hex(str)
	end
	local width
	if tp == "int32" then
		width = 4
	elseif tp == "int64" then
		width = 8
	end
	if str:find(",") then
		local parts = rtk.stringToArray(str, ",")
		local all = {}
		rtk.table_ieach(parts, function(_, p)
			local num = tonumber(p)
			table.insert(all, gx.int_to_hex(num, width))
		end)
		return table.concat(all)
	else
		local num = tonumber(str)
		assert(num ~= 0, "Cannot search 0 or invalid number")
		return gx.int_to_hex(num, width)
	end
end

function TMainDialogDef:cmbProcesses_onCombobox()
	local proc = gx.FProcesses[self.cmbProcesses.Selection + 1]
	gx.FProc = proc
	if gx.FhProcess then
		gx.closeHandle(gx.FhProcess)
	end
	gx.FhProcess = gx.openProcess(proc.id)
	self.lstAddresses:Clear()
	self.memEditor.gridMemory.Value = ""
end

function TMainDialogDef:refreshProcesses()
	local processes = gx.enumProcesses(".+")
	self.cmbProcesses:Clear()
	local windir = os.getenv("Windir")
	local procs2 = {}
	rtk.table_ieach(processes, function(_, p)
		if windir and p.exe:match("^" .. windir) then
			-- ignore system processes
			return
		end
		local str = string.format("%d(%s)", p.id, p.exe)
		self.cmbProcesses:Append(str)
		table.insert(procs2, p)
	end)
	gx.FProcesses = procs2
	rtk.printk(0, "%d processes refreshed.\n", #procs2)
end

function TMainDialogDef:btnRefreshProcesses_onClick()
	self:refreshProcesses()
end

function TMainDialogDef:updateAddressList(plist)
	self.lstAddresses:Clear()
	rtk.table_ieach(plist, function(_, addr)
		local buf = gx.readProcessMemory(gx.FhProcess, addr, 16)
		local line = string.format("%03d %016x : %s", _, addr, gx.tohex(buf))
		rtk.printk(0, "Found %s\n", line)
		self.lstAddresses:Append(line)
	end)
	gx.FAddresses = plist
end

function TMainDialogDef:btnSearch_onClick()
	local val = self.txtString.Value
	local vtype = self.cmbType.Value
	if not gx.FProc then
		return
	end
	rtk.printk(0, 'Searching "%s" in process %d(%s), type="%s"\n', val, gx.FProc.id, gx.FProc.exe, vtype)
	local pid = gx.FProc.id
	val = self:getSearchString()
	plist = gx.searchProcessString(pid, val)
	self:updateAddressList(plist)
end

function TMainDialogDef:btnRefine_onClick()
	local addresses = gx.FAddresses
	local sstring = self:getSearchString()
	local addresses2 = {}
	rtk.printk(0, "Refine search in %d entries.\n", #addresses)
	rtk.table_ieach(addresses, function(_, addr)
		local ns = gx:peek(addr, #sstring)
		if ns == sstring then
			table.insert(addresses2, addr)
		end
	end)
	self:updateAddressList(addresses2)
end

function TMainDialogDef:btnUpdate_onClick()
	local plist = gx.FAddresses
	self.lstAddresses:Clear()
	rtk.table_ieach(plist, function(_, addr)
		local buf = gx.readProcessMemory(gx.FhProcess, addr, 16)
		local line = string.format("%03d %016x : %s", _, addr, gx.tohex(buf))
		rtk.printk(0, "Found %s\n", line)
		self.lstAddresses:Append(line)
	end)
end

function TMainDialogDef:lstAddresses_onListbox()
	local sel = self.lstAddresses:GetSelection() + 1
	local addr = gx.FAddresses[sel]
	self.memEditor:go(addr)
end

function TMainDialogDef:doCreate()
	self:refreshProcesses()
	gx.updateAddressList = function(lst)
		self:updateAddressList(lst)
	end
	local vtypes = { "lua", "string", "string(gbk)", "hex", "int32", "int64" }
	rtk.table_ieach(vtypes, function(_, t)
		self.cmbType:Append(t)
	end)
	self.cmbType:SetSelection(0)
	self.lstAddresses:SetFont(font)
	rtk.table_ieach(gx.getInterpreters(), function(_, itp)
		self.memEditor:doAddInterpreter(itp)
	end)
	self.memEditor.cmbInterpreters:SetSelection(0)
end

function TMainDialogDef:onCloseWindow()
	wx.wxGetApp():ExitMainLoop()
end

function TMainDialogDef:edit(sel)
	local addr
	if not sel then
		addr = nil
	elseif sel >= 1 and sel <= self.lstAddresses.Count then
		addr = self.lstAddresses[sel]
	else
		addr = sel
	end
	self.memEditor:doInterprete(nil, addr)
end

local function load_lua_file(f)
	local code, err
	local fh
	rtk.dbg_print("Loading plugin %s\n", f)
	fh = io.open(f, "rb")
	assert(fh, "Cannot open " .. f)
	code = fh:read("*a")
	fh:close()
	code, err = load(code)
	assert(code, string.format("Error open:%s,", f, err))
	code, err = xpcall(code, debug.traceback)
	assert(code, err)
end

local function load_plugins()
	local lfs = require("lfs")
	local dir
	local plugin_dir = rtk.getini("gamex", "plugin_dir") or (rtk.getWorkingDir() .. "/bin/lua/games")
	for dir in lfs.dir(plugin_dir) do
		local fpath = plugin_dir .. "/" .. dir
		if lfs.attributes(fpath, "mode") == "directory" then
			local f = fpath .. "/" .. dir .. ".lua"
			if rtk.fileExists(f) then
				load_lua_file(f)
			else
				f = fpath .. "/" .. dir .. ".ljc"
				if rtk.fileExists(f) then
					load_lua_file(f)
				end
			end
		end
	end
end

local function setCodePage()
	if os.getenv("windir") and rtk.getpropFlag("debug") then
		local ffi = require("ffi")
		ffi.cdef("char SetConsoleOutputCP(unsigned int);")
		ffi.C.SetConsoleOutputCP(65001)
	end
end

function main(...)
	load_plugins()
	setCodePage()
	local dlg = TMainDialogDef:create()
	tdialog = dlg
	dlg:Show()
	wx.wxGetApp():MainLoop()
end

if not rtk.getpropFlag("#.startAsExe") then
	main()
end
